Subsystem 1a: Discord workspace provisioning & configuration - #4
Merged
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rkspace provisioning) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ueries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… assembly seam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces WorkspaceServiceCollectionExtensions.AddWorkspace() wiring all workspace singletons and scoped services, validated by a ValidateScopes=true composition test that proves the full DI graph resolves without captive-dependency violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…elDestroyed, ServerRegistered)
…es, skip empty payloads - HealGuildAsync now heals whichever scopes are provisioned (global-if-present + each provisioned server category) instead of gating on the global category, so a per-server-only guild (e.g. simulate-server without /setup) self-heals; still no-ops when nothing is provisioned, preserving reset resurrection-safety. - EnsureMessagesAsync skips posting an empty payload (a renderer whose source entity vanished mid-reconcile) since Discord rejects a message with no content/embed/components. - Document the in-process event bus no-replay window on the ServerRegistered consumer.
There was a problem hiding this comment.
Pull request overview
Implements “Subsystem 1a” by introducing a desired-state Discord workspace provisioning system (categories/channels/anchored messages) and removing the previous admin-wired /server + /bind workflow. This adds a new RustPlusBot.Features.Workspace feature project, a persistence model for provisioned Discord resources, and host wiring so the bot can reconcile and self-heal its workspace.
Changes:
- Added a workspace spec registry + reconciler (+ teardown) with Discord.Net gateway seam, hosted startup/self-heal behavior, and admin/dev interaction modules (
/setup,/workspace ..., language selector). - Added EF Core persistence for provisioned categories/channels/messages (and migration) and wired
IWorkspaceStoreinto DI; extendedIServerServicewithGetAsync. - Removed legacy channel binding and server-management interaction modules and their persistence/tests; updated docs and solution/project references accordingly.
Reviewed changes
Copilot reviewed 91 out of 92 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs | Adds coverage for WorkspaceStore upsert/delete/culture behaviors. |
| tests/RustPlusBot.Persistence.Tests/Workspace/ProvisioningSchemaTests.cs | Verifies cascade delete + global scope persistence in schema. |
| tests/RustPlusBot.Persistence.Tests/PersistenceRegistrationTests.cs | Updates DI assertions after removing bindings. |
| tests/RustPlusBot.Persistence.Tests/Bindings/BindingServiceTests.cs | Removes tests for deleted binding subsystem. |
| tests/RustPlusBot.Features.Workspace.Tests/WorkspaceRegistrationTests.cs | Validates scoped resolution of reconciler/teardown via DI. |
| tests/RustPlusBot.Features.Workspace.Tests/Teardown/WorkspaceTeardownServiceTests.cs | Covers reset/remove-server teardown flows. |
| tests/RustPlusBot.Features.Workspace.Tests/RustPlusBot.Features.Workspace.Tests.csproj | Introduces new test project for workspace feature. |
| tests/RustPlusBot.Features.Workspace.Tests/Registry/WorkspaceRegistryTests.cs | Covers registry aggregation and ordering behavior. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerServerTests.cs | Tests per-server provisioning and unknown-server behavior. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerSelfHealTests.cs | Tests recreate behavior after external deletion. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerProvisionTests.cs | Tests fresh provisioning and permission preflight. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerMessageTests.cs | Tests message repost vs edit behavior and orphan retention. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerIdempotencyTests.cs | Ensures reconciles are idempotent. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerHealTests.cs | Tests healing behavior and reset non-resurrection. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerConcurrencyTests.cs | Tests per-guild serialization outcome under concurrency. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/WorkspaceReconcilerAdoptTests.cs | Tests adopt path when same-named channel exists. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs | Adds harness + fakes wiring for reconciler tests. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ProvisioningLockTests.cs | Tests guild-scoped lock serialization. |
| tests/RustPlusBot.Features.Workspace.Tests/Placeholder.cs | Adds placeholder compilation test for new project. |
| tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs | Tests renderer output (information/settings/server info). |
| tests/RustPlusBot.Features.Workspace.Tests/Localization/LocalizerTests.cs | Tests i18n fallback/normalization/formatting. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceStore.cs | In-memory workspace store for unit tests. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceGatewayTests.cs | Tests fake gateway round-tripping and deletion. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceGateway.cs | In-memory gateway used by reconciler tests. |
| src/RustPlusBot.Persistence/Workspace/WorkspaceStore.cs | EF Core implementation of IWorkspaceStore. |
| src/RustPlusBot.Persistence/Workspace/IWorkspaceStore.cs | Defines persistence API for provisioned workspace resources. |
| src/RustPlusBot.Persistence/Servers/ServerService.cs | Adds GetAsync to support server-scoped reconciliation. |
| src/RustPlusBot.Persistence/Servers/IServerService.cs | Adds GetAsync contract for server lookup by guild+id. |
| src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs | Registers IWorkspaceStore; removes binding service registration. |
| src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs | Updates snapshot for provisioning tables and removed bindings. |
| src/RustPlusBot.Persistence/Migrations/20260614195321_WorkspaceProvisioning.Designer.cs | Adds migration model for workspace provisioning schema. |
| src/RustPlusBot.Persistence/Migrations/20260614195321_WorkspaceProvisioning.cs | Adds migration creating provisioning tables and dropping bindings. |
| src/RustPlusBot.Persistence/Configurations/ProvisionedMessageConfiguration.cs | Configures EF mapping for ProvisionedMessage. |
| src/RustPlusBot.Persistence/Configurations/ProvisionedChannelConfiguration.cs | Configures EF mapping for ProvisionedChannel. |
| src/RustPlusBot.Persistence/Configurations/ProvisionedCategoryConfiguration.cs | Configures EF mapping for ProvisionedCategory. |
| src/RustPlusBot.Persistence/Configurations/ChannelBindingConfiguration.cs | Removes EF config for deleted ChannelBinding. |
| src/RustPlusBot.Persistence/BotDbContext.cs | Adds provisioning DbSets/configs; removes channel bindings DbSet/config. |
| src/RustPlusBot.Persistence/Bindings/IBindingService.cs | Removes binding service interface. |
| src/RustPlusBot.Persistence/Bindings/BindingService.cs | Removes binding service implementation. |
| src/RustPlusBot.Host/RustPlusBot.Host.csproj | References workspace feature project. |
| src/RustPlusBot.Host/Program.cs | Binds Workspace options and registers workspace feature. |
| src/RustPlusBot.Host/appsettings.json | Adds Workspace:EnableDangerCommands configuration. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | DI wiring for workspace feature, gateway, hosted service, modules. |
| src/RustPlusBot.Features.Workspace/WorkspaceOptions.cs | Adds options model for danger-command gating. |
| src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs | Defines stable channel/message keys for persistence. |
| src/RustPlusBot.Features.Workspace/Teardown/WorkspaceTeardownService.cs | Implements reset/remove scope deletion under provisioning lock. |
| src/RustPlusBot.Features.Workspace/Teardown/IWorkspaceTeardownService.cs | Defines teardown service contract. |
| src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs | Provides per-server desired-state specs. |
| src/RustPlusBot.Features.Workspace/Specs/GlobalWorkspaceSpecProvider.cs | Provides global desired-state specs. |
| src/RustPlusBot.Features.Workspace/RustPlusBot.Features.Workspace.csproj | Introduces new feature project and dependencies. |
| src/RustPlusBot.Features.Workspace/Registry/WorkspaceScope.cs | Defines global vs per-server scopes. |
| src/RustPlusBot.Features.Workspace/Registry/WorkspaceRegistry.cs | Aggregates contributed specs into ordered views. |
| src/RustPlusBot.Features.Workspace/Registry/MessageSpec.cs | Declarative anchored-message spec. |
| src/RustPlusBot.Features.Workspace/Registry/MessageRenderContext.cs | Context for message rendering. |
| src/RustPlusBot.Features.Workspace/Registry/IWorkspaceRegistry.cs | Interface for aggregated spec registry. |
| src/RustPlusBot.Features.Workspace/Registry/IMessageSpecProvider.cs | DI contribution interface for message specs. |
| src/RustPlusBot.Features.Workspace/Registry/IMessageRenderer.cs | Renderer interface keyed by stable message key. |
| src/RustPlusBot.Features.Workspace/Registry/IChannelSpecProvider.cs | DI contribution interface for channel specs. |
| src/RustPlusBot.Features.Workspace/Registry/ChannelSpec.cs | Declarative channel spec (name key, perms, order). |
| src/RustPlusBot.Features.Workspace/Registry/ChannelPermissionProfile.cs | Permission profile enum for provisioned channels. |
| src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs | Core reconciler (resolve/adopt/create) and self-heal logic. |
| src/RustPlusBot.Features.Workspace/Reconciler/ReconcileResult.cs | Result model for reconcile outcomes and missing perms. |
| src/RustPlusBot.Features.Workspace/Reconciler/ProvisioningLock.cs | Guild-scoped semaphore lock implementation. |
| src/RustPlusBot.Features.Workspace/Reconciler/IWorkspaceReconciler.cs | Reconciler interface (global/server/heal). |
| src/RustPlusBot.Features.Workspace/Reconciler/IProvisioningLock.cs | Provisioning lock interface. |
| src/RustPlusBot.Features.Workspace/Modules/WorkspaceAdminModule.cs | Adds dev/admin commands (/workspace reset, /workspace simulate-server). |
| src/RustPlusBot.Features.Workspace/Modules/SetupModule.cs | Adds /setup to provision global + per-server workspace. |
| src/RustPlusBot.Features.Workspace/Modules/SettingsComponentModule.cs | Handles language select menu and triggers re-render. |
| src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs | Renders global setup embed. |
| src/RustPlusBot.Features.Workspace/Messages/SettingsMessageRenderer.cs | Renders settings embed + language selector components. |
| src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs | Renders per-server info embed (endpoint). |
| src/RustPlusBot.Features.Workspace/Messages/InformationMessageRenderer.cs | Renders global information embed incl. server count. |
| src/RustPlusBot.Features.Workspace/Localization/Localizer.cs | Implements culture normalization + fallback i18n. |
| src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs | Adds built-in EN/FR localization catalog. |
| src/RustPlusBot.Features.Workspace/Localization/ILocalizer.cs | Localizer interface. |
| src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs | Startup reconcile + channel deletion self-heal + server-registered consumer. |
| src/RustPlusBot.Features.Workspace/Gateway/MessagePayload.cs | Message payload model (text/embed/components). |
| src/RustPlusBot.Features.Workspace/Gateway/IWorkspaceGateway.cs | Gateway interface over Discord primitives. |
| src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs | Discord.Net implementation of gateway operations. |
| src/RustPlusBot.Features.Workspace/AssemblyInfo.cs | Exposes internals to workspace tests. |
| src/RustPlusBot.Domain/Workspace/ProvisionedMessage.cs | Adds domain entity for anchored messages. |
| src/RustPlusBot.Domain/Workspace/ProvisionedChannel.cs | Adds domain entity for provisioned channels. |
| src/RustPlusBot.Domain/Workspace/ProvisionedCategory.cs | Adds domain entity for provisioned categories. |
| src/RustPlusBot.Domain/Guilds/ChannelBinding.cs | Removes legacy binding entity. |
| src/RustPlusBot.Domain/Guilds/BoundFeature.cs | Removes legacy bound-feature enum. |
| src/RustPlusBot.Discord/Modules/ServerModule.cs | Removes legacy /server interaction module. |
| src/RustPlusBot.Discord/Modules/BindModule.cs | Removes legacy /bind interaction module. |
| src/RustPlusBot.Discord/InteractionModuleAssembly.cs | Adds assembly marker for module discovery across feature projects. |
| src/RustPlusBot.Discord/DiscordBotService.cs | Scans additional module assemblies to load feature interactions. |
| src/RustPlusBot.Abstractions/Events/ServerRegisteredEvent.cs | Introduces event used to trigger per-server provisioning. |
| RustPlusBot.slnx | Adds workspace feature + tests projects to solution. |
| docs/development/running-locally.md | Updates local run instructions for /setup + workspace provisioning flow. |
Files not reviewed (1)
- src/RustPlusBot.Persistence/Migrations/20260614195321_WorkspaceProvisioning.Designer.cs: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+23
to
+50
| public bool CategoryExists(ulong guildId, ulong categoryId) => _categories.ContainsKey(categoryId); | ||
|
|
||
| public Task<ulong?> FindCategoryAsync(ulong guildId, string name, CancellationToken cancellationToken) | ||
| { | ||
| var match = _categories.Values.FirstOrDefault(c => | ||
| string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); | ||
| return Task.FromResult(match is null ? (ulong?)null : match.Id); | ||
| } | ||
|
|
||
| public Task<ulong> CreateCategoryAsync(ulong guildId, string name, CancellationToken cancellationToken) | ||
| { | ||
| var id = NextId(); | ||
| _categories[id] = new Category(id, name); | ||
| CreatedCategories++; | ||
| return Task.FromResult(id); | ||
| } | ||
|
|
||
| public bool ChannelExists(ulong guildId, ulong channelId) => _channels.ContainsKey(channelId); | ||
|
|
||
| public Task<ulong?> FindChannelAsync(ulong guildId, | ||
| ulong categoryId, | ||
| string name, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| var match = _channels.Values.FirstOrDefault(c => | ||
| c.CategoryId == categoryId && string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); | ||
| return Task.FromResult(match is null ? (ulong?)null : match.Id); | ||
| } |
| public const string ConfirmResetId = "workspace:reset:confirm"; | ||
|
|
||
| /// <summary>Prompts to delete the entire provisioned workspace (dev-gated).</summary> | ||
| [SlashCommand("reset", "Delete ALL provisioned channels and records for this server (dangerous)")] |
| return; | ||
| } | ||
|
|
||
| var culture = selectedValues.Length > 0 ? selectedValues[0] : "en"; |
Comment on lines
+182
to
+184
| var adopted = await gateway.FindChannelAsync(guildId, categoryId, name, cancellationToken) | ||
| .ConfigureAwait(false); | ||
| if (adopted is ulong adoptedId) |
…set wording, Français label - SettingsComponentModule: only persist supported cultures (en/fr) from the (forgeable) component payload, defaulting to en, so an overlong/unknown value can't break GuildSettings.Culture (max 16). - WorkspaceAdminModule: validate simulate-server port is 1-65535; clarify /workspace reset description to 'this Discord server' to disambiguate from a Rust server. - SettingsMessageRenderer: correct the user-facing language label to 'Français'.
Owner
Author
|
Thanks @copilot — addressed in 4c25b01:
Two I'm intentionally leaving for now:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements subsystem 1a — the bot now owns and provisions its own Discord workspace (categories, channels, anchored messages) instead of asking admins to wire them up. This replaces the foundation's
/serverand/bindcommands, which were the wrong model for this product.What changed
/server add|list|remove,/bind, theChannelBindingentity/service, and their tests. Servers are no longer typed in by hand — they register via aServerRegisteredEvent(a stubbed dev trigger in 1a; real FCM pairing lands in 1b).RustServer/ServerServiceare kept.RustPlusBot.Features.Workspaceproject built around a declarative desired-state reconciler:ChannelSpec/MessageSpecvia DI ("grow per subsystem"), so later features just register their channels.IWorkspaceGatewayseam over Discord.Net, so the whole engine is unit-tested against an in-memory fake (no Discord needed).WorkspaceReconcilerconverges desired (registry) vs actual (stored snowflakes + live Discord) via resolve → adopt → create, serialized per guild byProvisioningLock, with a bot-permission pre-flight. Idempotent, drift-adopting, and self-healing.Provisioned*tables (cascade FK toRustServer, unique per scope+key) +WorkspaceStore, and aWorkspaceProvisioningmigration that dropsChannelBindings./setup(admin-gated) provisions the global RustPlusBot category (#information/#setup/#settings); a#settingslanguage selector (EN/FR) re-renders the workspace; dev-gated/workspace resetand/workspace simulate-server. i18n seam landed (EN + FR).ChannelDestroyedself-heal (reset-safe — won't resurrect a workspace cleared by/workspace reset), and aServerRegisteredEventconsumer that provisions a per-server category + static#info.AddWorkspace, aWorkspace:EnableDangerCommandsflag (off in prod, on in Development), and anInteractionModuleAssemblyseam so feature projects' slash commands get discovered.49 tests (reconciler idempotency/adopt/self-heal/concurrency/per-server, store, teardown, localizer, renderers, DI composition). Build is 0 warnings / 0 errors under the strict analyzer set (NetAnalyzers/Roslynator/Sonar,
TreatWarningsAsErrors).Test Plan
Automated (in CI):
dotnet build RustPlusBot.slnx(0/0) anddotnet test RustPlusBot.slnx(49 passing).Manual smoke (requires a bot token + invite with Manage Channels/Roles/Messages + Embed Links):
/setupcreates the RustPlusBot category with#information/#setup/#settings, read-only to members, embeds rendered./setupproduces no duplicates.#information→ it reappears (self-heal).#settingsre-renders embeds in FR./workspace simulate-server name:Test ip:1.1.1.1 port:28015→ a Test category with#infoappears./workspace reset→ confirm → everything is deleted and does not reappear.🤖 Generated with Claude Code